Assertions.php

<?php

namespace Tlf;

trait Assertions {

    protected $inverted = false;

    public function __call($method, $args){
        if (!function_exists($method)){
            throw new \Exception("'$method' is not a valid assertion, but you can use any existing function as an assertion if it returns truthy/falsey values.");
        }

        $didPass = $method(...$args);
        $this->handleDidPass($didPass);
        $c = count($args);
        echo "'$method'([$c arguments]) ";

    }

    /**
     * Invert test passage so failing tests actually passes them
     */
    public function invert(){
        $this->inverted = !$this->inverted;
        return $this->inverted;
    }

    public function isInstanceOf($object, $shouldBe){
            $class = get_class($object);
        if ($object instanceof $shouldBe){
            $this->handleDidPass(true);
            echo "'$class' is instanceof '$shouldBe'";
        } else {
            $this->handleDidPass(false);
            echo "'$class' is NOT instanceof '$shouldBe'";
        }
    }
    public function is_object($object){
        if (is_object($object)){
            $this->handleDidPass(true);
            echo "Is an object";
        } else {
            $this->handleDidPass(false);
            echo "Is not an object";
        }
    }

    public function is_false($value){
        $this->handleDidPass($value === false);
        echo "is_false()";
    }
    public function is_true($value){
        $this->handleDidPass($value === true);
        echo "is_true()";
    }

    public function str_contains($str, $target){
        if (is_array($target)){
            $success = true;
            foreach ($target as $strTarget){
                $oneSuccess = $this->str_contains($str,$strTarget);
                if (!$oneSuccess)$success = false;
            }
            return $success;
        }
        $pass = false;
        if (strpos($str,$target)!==false)$pass=true;

        $this->handleDidPass($pass);
        if ($pass){
            echo "String contains '$target'";
        } else {
            echo "String does not contain '$target'";
        }
        return false;
    }
    public function str_not_contains(string $str, $target){
        if (is_array($target)){
            $success = true;
            foreach ($target as $strTarget){
                $oneSuccess = $this->str_not_contains($str,$strTarget);
                if (!$oneSuccess)$success = false;
            }
            return $success;
        } else $target = (string)$target;
        $pass = true;
        if (strpos($str,$target)!==false)$pass=false;

        $this->handleDidPass($pass);
        if ($pass){
            echo "String does not contain '$target'";
        } else {
            echo "String contains '$target'";
        }
        return false;
    
    }

    public function catch($exceptionClass,$strict=false){
        $catcher = new Tester\ExceptionCatcher($exceptionClass);
        $this->catchers[] = $catcher;
        return $catcher;
    }
    public function throw($e){
        $list = $this->catchers;
        foreach ($list as $index => $cat){
            if ($cat->matches($e)){
                //@TODO allow one catcher to be re-used & just require that each exception be caught by at least one.
                unset($this->catchers[$index]);
                ob_start();
                $this->handleDidPass(true);
                ob_get_clean();
                // "Exception was caught";
                return true;
            }
        }
        $this->handleDidPass(false);
        echo "An exception was not handled...";
        throw $e;
    }

    public function compare($target, $actual,$strict=false){
        if (!$strict&&is_string($target)&&substr($target,0,7)=='file://'){
            $file = substr($target,7);
            if (!is_file($file)){
                throw new \Exception("{$target} is not a file. ");
            }
            $ext = substr($file,-4);
            if ($ext=='.php'){
                ob_start();
                require($file);
                $target=ob_get_clean();
            }
            else $target=file_get_contents($file);
        }
        if (!$strict&&is_string($actual)&&substr($actual,0,7)=='file://'){
            $file = substr($actual,7);
            if (!is_file($file)){
                throw new \Exception("{$actual} is not a file. ");
            }
            $ext = substr($file,-4);
            if ($ext=='.php'){
                ob_start();
                require($file);
                $actual=ob_get_clean();
            }
            else $actual=file_get_contents($file);
        }


        if (!$strict&&is_string($target)&&is_string($actual)){
            $target = trim($target);
            $actual = trim($actual);
        }
        $pass = false;
        if ($strict&&($target===$actual))$pass = true;
        else if (!$strict&&$target==$actual)$pass = true;


        $target = $this->comparisonOutput($target);
        $actual = $this->comparisonOutput($actual);
        
        // echo "Strict: ".($strict ? 'true' : 'false');

        $this->handleDidPass($pass);
        if ($strict)echo " strict comparison";
        $this->targetVsActualOutput($target, $actual);

        return $pass;
    }

    /**
     * Simply compare the values 
     * @param $target the value you want
     * @param $actual the value you have
     * @param $strict TRUE to use `===`. FALSE to use `==`
     */
    public function compare_raw($target, $actual, $strict=false){
        $pass = $strict ? $target === $actual
                        : $target == $actual;

        ob_start();
        var_dump($target);
        $target_str = substr(ob_get_clean(), 0,500);
        ob_start();
        var_dump($actual);
        $actual_str = substr(ob_get_clean(), 0,500);

        $this->handleDidPass($pass);
        $this->targetVsActualOutput(
            $target_str,
            $actual_str
        );
        return $pass;
    }


    public function handleDidPass(bool $didPass){
        $extra='';
        if ($this->inverted){
            $didPass = !$didPass;
            $extra=" (inverted)";
        }
        echo "\n\n";
        if ($didPass)echo "+++pass+++$extra";
        else echo "---fail---$extra";
        echo "\n";
        $passStr = $didPass ? 'true' : 'false';
        $this->comparisons[$passStr]++;
        return $didPass;
    }

    public function targetVsActualOutput($target, $actual){
        // echo "\n";
        echo "Target:\n{$target}";
        echo "\n--\n";
        echo "Actual:\n{$actual}";
        echo "\n--------\n";
    }
    public function comparisonOutput($value){
        if (is_object($value)){
            return "Object of class ".get_class($value);
        }
        if (is_array($value)){

            array_walk_recursive($value, function(&$value){
                $value = str_replace(["\r","\n"],['\r','\n'], $value);
            });

            $pr = var_export($value, true);
            $maxLen= 1000;
            
            if (($this->options['prettyPrintArray'][0]??null)=='true'){
                $oneLine = $pr;
                $maxLen = 1500;
            } else {
                $oneLine = implode(" ",array_map('trim',explode("\n",$pr)));
                $oneLine = str_replace('\\\\n', '\n', $oneLine);
            }
            $value = substr($oneLine,0,$maxLen);
            if (strlen($oneLine)>$maxLen)$value .= '...';

            return $value;
        }

        if ($value===true)return 'true';
        else if ($value===false)return 'false';

        return $value;
    }

}